12+

Sections

60+

Concepts

v17+

Modern Angular

๐Ÿ—๏ธ Angular's Building Blocks (Big Picture)

An Angular app = a tree of Components. Components have Templates (HTML). Services handle logic & data. Modules group related things. Routing maps URLs to components. Directives add behavior to HTML. Pipes transform display data. RxJS handles async streams. DI provides dependencies where needed.

@Component Decorator โ€” Every Property
CORE
Component decorator โ€” all optionsTypeScript
@Component({
  // REQUIRED
  selector: 'app-user-card',       // CSS selector: element/attr/class
  templateUrl: './user-card.html',  // OR template: `...` inline
  styleUrls: ['./user-card.scss'],  // OR styles: [`...`] inline

  // CHANGE DETECTION
  changeDetection: ChangeDetectionStrategy.OnPush, // PERFORMANCE!

  // ENCAPSULATION
  encapsulation: ViewEncapsulation.Emulated, // default: adds unique attrs
  // ViewEncapsulation.None   โ†’ styles are global
  // ViewEncapsulation.ShadowDom โ†’ real shadow DOM

  // STANDALONE (Angular 14+)
  standalone: true,
  imports: [CommonModule, RouterModule],  // for standalone

  // HOST
  host: { 'class': 'my-component', '(click)': 'onClick()' },

  // ANIMATIONS
  animations: [trigger('fadeIn', [...])],
})
@Input() โ€” All Variants
INPUTS
@Input variations โ€” TypeScriptTypeScript
// Basic
@Input() title: string = '';

// With alias (parent uses 'data', internally it's 'item')
@Input('data') item!: User;

// Required (Angular 16+) โ€” throws error if not passed
@Input({ required: true }) userId!: string;

// Transform (auto-convert string โ†’ number from template)
@Input({ transform: numberAttribute }) count!: number;
@Input({ transform: booleanAttribute }) disabled!: boolean;

// With setter โ€” run logic when value changes
private _name = '';
@Input()
set name(val: string) {
  this._name = val.trim().toUpperCase();
}
get name() { return this._name; }

// Signal input (Angular 17+)
count = input(0);           // optional, default=0
name  = input.required<string>();  // required signal input
@Output() & EventEmitter
OUTPUTS
@Output patternsTypeScript
// Child component
export class UserCardComponent {
  @Output() selected = new EventEmitter<User>();
  @Output() deleted = new EventEmitter<string>();  // emit ID

  onSelect(user: User) {
    this.selected.emit(user);
  }
}

// Parent template
<app-user-card
  (selected)="handleSelect($event)"
  (deleted)="removeUser($event)"
/>

// Parent component
handleSelect(user: User) { /* $event is the User object */ }

// output() โ€” Signal API (Angular 17+)
export class NewCardComponent {
  selected = output<User>();
  onSelect(u: User) { this.selected.emit(u); }
}
@ViewChild & @ContentChild
QUERIES

@ViewChild: Access a child component, directive, or DOM element from the component's own template.

@ContentChild: Access projected content (ng-content) passed INTO this component.

ViewChild / ContentChildTypeScript
export class ParentComponent implements AfterViewInit {
  // Get child COMPONENT instance
  @ViewChild(ChildComponent) child!: ChildComponent;

  // Get DOM element via template ref
  @ViewChild('myInput') inputEl!: ElementRef<HTMLInputElement>;

  // All matching children (plural)
  @ViewChildren(ChildComponent) children!: QueryList<ChildComponent>;

  // NOT available in constructor or ngOnInit!
  ngAfterViewInit() {
    this.inputEl.nativeElement.focus();  // โœ… Safe here
    this.child.someMethod();
  }
}

// ContentChild: content projected via <ng-content>
@ContentChild(HeaderComponent) header!: HeaderComponent;
// Available in ngAfterContentInit()
Content Projection โ€” ng-content
PROJECTION
ng-content is like a "slot" โ€” think of a modal component that you can fill in with any content from the parent. Like Angular Material's mat-dialog-content slot.
Content Projection patternsHTML + TypeScript
<!-- card.component.html -->
<div class="card">
  <div class="card-header">
    <ng-content select="[card-title]"></ng-content>  <!-- named slot -->
  </div>
  <div class="card-body">
    <ng-content></ng-content>  <!-- default slot -->
  </div>
</div>

<!-- Parent usage -->
<app-card>
  <h2 card-title>My Title</h2>  <!-- goes to named slot -->
  <p>This goes in the default slot</p>
</app-card>

<!-- ngTemplateOutlet: dynamic templates -->
<ng-container *ngTemplateOutlet="myTemplate; context: {$implicit: item}">
</ng-container>
Built-in Structural Directives
DIRECTIVES
Structural directivesHTML
<!-- *ngIf (old) / @if (new Angular 17+) -->
<div *ngIf="isLoggedIn; else guestBlock">Welcome!</div>
<ng-template #guestBlock><p>Please login</p></ng-template>

@if (isLoggedIn) { <p>Welcome!</p> } @else { <p>Login</p> }

<!-- *ngFor / @for -->
<li *ngFor="let item of items; let i=index; trackBy: trackById">
  {{i}}: {{item.name}}
</li>

@for (item of items; track item.id) {
  <li>{{item.name}}</li>
} @empty { <p>No items</p> }

<!-- *ngSwitch / @switch -->
<div [ngSwitch]="status">
  <p *ngSwitchCase="'active'">Active</p>
  <p *ngSwitchDefault>Unknown</p>
</div>
trackBy is CRITICAL for *ngFor performance! Without it, Angular re-renders every item on any array change, even if only one item changed.
Built-in Attribute Directives
DIRECTIVES
Attribute directivesHTML
<!-- [ngClass] โ€” dynamic class binding -->
<div [ngClass]="{'active': isActive, 'disabled': !enabled}">
<div [ngClass]="getClasses()">  <!-- method returning object/array/string -->

<!-- [ngStyle] โ€” dynamic inline styles -->
<div [ngStyle]="{'color': textColor, 'font-size.px': fontSize}">

<!-- [class.xxx] โ€” cleaner single class binding -->
<div [class.active]="isActive" [class.error]="hasError">

<!-- [style.xxx] โ€” cleaner single style binding -->
<div [style.color]="primaryColor" [style.fontSize.px]="size">

<!-- ngModel โ€” two-way binding (needs FormsModule) -->
<input [(ngModel)]="username" (ngModelChange)="onNameChange($event)">
Custom Directive โ€” Full Example
CUSTOM
Custom attribute directiveTypeScript
@Directive({
  selector: '[appHighlight]',  // used as attribute
  standalone: true
})
export class HighlightDirective {
  @Input('appHighlight') color = 'yellow';
  @Input() defaultColor = 'white';

  constructor(private el: ElementRef, private renderer: Renderer2) {}

  // HostListener: listen to DOM events on the host element
  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.color);
  }
  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(this.defaultColor);
  }

  private highlight(color: string) {
    // โœ… Use Renderer2, NOT direct DOM โ€” works in SSR!
    this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', color);
  }
}

<!-- Usage -->
<p appHighlight="cyan" defaultColor="white">Hover me!</p>
Template Reference Variables & ng-template
TEMPLATES
Template refsHTML + TypeScript
<!-- #ref on element โ†’ get DOM element -->
<input #nameInput type="text">
<button (click)="nameInput.focus()">Focus</button>

<!-- #ref on component โ†’ get component instance -->
<app-child #child></app-child>
<button (click)="child.reset()">Reset Child</button>

<!-- ng-template: a lazy template, not rendered by default -->
<ng-template #loadingTpl>
  <div class="spinner">Loading...</div>
</ng-template>

<!-- ng-container: grouping without adding DOM element -->
<ng-container *ngIf="isLoggedIn">
  <app-nav></app-nav>
  <app-header></app-header>
</ng-container>

<!-- Render ng-template dynamically -->
<ng-container *ngTemplateOutlet="loading ? loadingTpl : contentTpl">
</ng-container>
All 4 Binding Types
BINDING
Data binding cheatsheetHTML
<!-- 1. Interpolation: component โ†’ view (one-way) -->
<h1>{{title}}</h1>
<p>{{1 + 1}}</p>  <!-- expressions OK -->
<p>{{user?.name | uppercase}}</p>  <!-- optional chaining + pipe -->

<!-- 2. Property binding: component โ†’ view (one-way) -->
<img [src]="imageUrl" [alt]="imageAlt">
<button [disabled]="isLoading">Save</button>
<app-user [user]="currentUser"></app-user>

<!-- 3. Event binding: view โ†’ component (one-way) -->
<button (click)="save()">Save</button>
<input (input)="onInput($event)" (blur)="validate()">
<div (keydown.enter)="submit()" (keydown.escape)="cancel()">

<!-- 4. Two-way binding: both directions -->
<input [(ngModel)]="username">
<!-- [(ngModel)] is syntax sugar for: -->
<input [ngModel]="username" (ngModelChange)="username=$event">

<!-- Custom two-way on your own component: -->
<app-counter [(count)]="myCount"></app-counter>
<!-- Requires: @Input() count + @Output() countChange -->
Safe Navigation & Async Pipe
BINDING
async pipe & safe navigationHTML
<!-- async pipe: auto-subscribes and unsubscribes! -->
<div *ngIf="user$ | async as user">
  {{user.name}}
</div>

<!-- Multiple async with one subscription -->
<ng-container *ngIf="{users: users$ | async, loading: loading$ | async} as vm">
  <div *ngIf="!vm.loading">
    <div *ngFor="let u of vm.users">{{u.name}}</div>
  </div>
</ng-container>

<!-- Safe navigation operator ?. -->
{{user?.address?.city}}  <!-- won't crash if user is null -->
<p>{{items?.[0]?.name}}</p>

<!-- Non-null assertion -->
{{user!.name}}  <!-- tell TS: "I know this isn't null" -->
โญ async pipe benefits
  • Auto-unsubscribes on component destroy (no memory leaks)
  • Triggers change detection when value arrives
  • Works with both Observables and Promises
@Injectable & Provider Scopes
DI
Injectable scopesTypeScript
// providedIn: 'root' โ€” SINGLETON, app-wide (most common)
@Injectable({ providedIn: 'root' })
export class UserService { ... }

// providedIn: 'any' โ€” one instance per lazy-loaded module
@Injectable({ providedIn: 'any' })
export class CacheService { ... }

// Component-level: new instance per component
@Component({ providers: [FormService] })   // fresh instance each time

// Multiple injection tokens
const API_URL = new InjectionToken<string>('api-url');

// Inject pattern (Angular 14+ โ€” no constructor needed!)
export class UserComponent {
  private userService = inject(UserService);  // โœ… Clean!
  private router = inject(Router);
  private apiUrl = inject(API_URL);
}
Service Patterns โ€” Best Practices
PATTERNS
Service with state + APITypeScript
@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);

  // State: BehaviorSubject holds current value
  private users$ = new BehaviorSubject<User[]>([]);

  // Public read-only observable
  readonly users = this.users$.asObservable();

  loadUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users').pipe(
      tap(users => this.users$.next(users)),  // update state
      catchError(err => {
        console.error(err);
        return of([]);  // recover gracefully
      })
    );
  }

  getUserById(id: string): Observable<User> {
    return this.http.get<User>(`/api/users/${id}`);
  }
}
Observable, Subject, BehaviorSubject
RXJS CORE
Observable = a newspaper subscription. You sign up (subscribe), papers arrive (next), subscription cancelled (complete/error). Subject = a chat room โ€” you can listen AND send. BehaviorSubject = a chat room that shows new members the latest message immediately.
Subjects comparisonTypeScript
// Subject: no initial value, only future values
const events$ = new Subject<string>();

// BehaviorSubject: has current value, emits it immediately on subscribe
const user$ = new BehaviorSubject<User | null>(null);
user$.getValue();        // get current value synchronously
user$.next(newUser);     // emit new value
user$.asObservable();    // expose as read-only

// ReplaySubject: replays last N values to new subscribers
const history$ = new ReplaySubject<string>(5);  // last 5

// AsyncSubject: only emits LAST value when complete()
const result$ = new AsyncSubject<number>();

// Signal-based (Angular 17+)
const count = signal(0);
const doubled = computed(() => count() * 2);
effect(() => console.log(`Count: ${count()}`));
Essential RxJS Operators
OPERATORS
Most important operatorsTypeScript
import { map, filter, switchMap, mergeMap, exhaustMap,
         debounceTime, distinctUntilChanged, catchError,
         takeUntil, takeUntilDestroyed, tap, combineLatest,
         forkJoin, of, from, startWith, withLatestFrom } from 'rxjs';

// TRANSFORMATION
source$.pipe(map(x => x * 2))               // transform each value
source$.pipe(filter(x => x > 0))            // filter values
source$.pipe(tap(x => console.log(x)))        // side effect, don't transform

// FLATTENING (THE IMPORTANT ONES)
// switchMap: cancels previous inner obs, use for search/autocomplete
search$.pipe(switchMap(q => this.api.search(q)))

// mergeMap: runs all in parallel, use when ORDER doesn't matter
ids$.pipe(mergeMap(id => this.api.load(id)))

// concatMap: waits for each, use for sequential operations
actions$.pipe(concatMap(a => this.api.save(a)))

// exhaustMap: ignores new until current completes โ€” use for login button
loginClick$.pipe(exhaustMap(() => this.auth.login(creds)))

// COMBINATION
combineLatest([a$, b$]).pipe(map(([a, b]) => a + b))  // latest of all
forkJoin([api1$, api2$])  // wait for all to complete (like Promise.all)

// TIME
search$.pipe(debounceTime(300), distinctUntilChanged())  // search input!

// CLEANUP โ€” CRITICAL, prevents memory leaks
private destroy$ = new Subject<void>();
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
source$.pipe(takeUntil(this.destroy$)).subscribe(...)

// Angular 16+ โ€” automatic cleanup via DestroyRef
source$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(...)
switchMap vs mergeMap vs concatMap vs exhaustMap
FLATTENING
Quick Decision Guide
  • switchMap โ€” Autocomplete/search: cancel old, use latest query
  • mergeMap โ€” Parallel downloads: run all at once, order doesn't matter
  • concatMap โ€” Save queue: wait for each save before starting next
  • exhaustMap โ€” Login/submit button: ignore rapid clicks, wait for first
Hotel elevator button: switchMap = only last floor request counts. mergeMap = all floors open simultaneously. concatMap = goes to every floor in order. exhaustMap = won't start until it finishes its current trip.
Route Configuration โ€” All Options
ROUTING
Route config โ€” everythingTypeScript
const routes: Routes = [ { path: '', redirectTo: '/home', pathMatch: 'full' }, { path: 'home', component: HomeComponent }, // Route params { path: 'users/:id', component: UserDetailComponent }, // Query params: /search?q=angular&page=2 { path: 'search', component: SearchComponent }, // Lazy loading module (old way) { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }, // Lazy loading standalone (new way) { path: 'dashboard', loadComponent: () => import('./dashboard/dashboard.component').then(c => c.DashboardComponent) }, // With guards { path: 'settings', component: SettingsComponent, canActivate: [authGuard], canDeactivate: [unsavedChangesGuard], resolve: { userData: userResolver } }, // Nested routes { path: 'products', component: ProductsComponent, children: [ { path: '', component: ProductListComponent }, { path: ':id', component: ProductDetailComponent } ] }, { path: '**', component: NotFoundComponent } ];
Route Guards (Functional Style)
GUARDS
Guards โ€” Angular 15+ functionalTypeScript
// canActivate: block unauthenticated access
export const authGuard: CanActivateFn = (route, state) => {
  const auth = inject(AuthService);
  const router = inject(Router);
  if (auth.isLoggedIn()) return true;
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url }
  });
};

// canActivateChild: protects all child routes
export const adminGuard: CanActivateChildFn = () =>
  inject(AuthService).hasRole('admin');

// canDeactivate: warn before leaving with unsaved changes
export const unsavedGuard: CanDeactivateFn<EditComponent> = (component) => {
  if (!component.isDirty) return true;
  return confirm('Unsaved changes. Leave anyway?');
};

// resolve: pre-fetch data before activating route
export const userResolver: ResolveFn<User> = (route) => {
  return inject(UserService).getUserById(route.params['id']);
};
// Access in component: route.snapshot.data['userData']
Router Navigation & Reading Params
NAVIGATION
Router service patternsTypeScript
export class UserComponent {
  private router = inject(Router);
  private route = inject(ActivatedRoute);

  // Navigate programmatically
  goToUser(id: string) {
    this.router.navigate(['/users', id]);
    this.router.navigate(['/search'], {
      queryParams: { q: 'angular' },
      queryParamsHandling: 'merge'  // keep existing params
    });
  }

  // Read route params (reactive โ€” auto-updates)
  userId$ = this.route.paramMap.pipe(
    map(params => params.get('id')!)
  );

  // Read query params
  search$ = this.route.queryParamMap.pipe(
    map(p => p.get('q') ?? '')
  );

  // Snapshot (static โ€” only reads once)
  id = this.route.snapshot.params['id'];

  // Resolved data
  user = this.route.snapshot.data['userData'];

  // Subscribe to navigation events
  constructor() {
    this.router.events.pipe(
      filter(e => e instanceof NavigationEnd)
    ).subscribe(e => console.log(e));
  }
}
Reactive Forms โ€” FormGroup, FormControl, FormArray
REACTIVE
Reactive Forms complete exampleTypeScript
export class UserFormComponent { private fb = inject(FormBuilder); form = this.fb.group({ name: ['', [Validators.required, Validators.minLength(3)]], email: ['', [Validators.required, Validators.email]], age: [null, [Validators.min(18), Validators.max(100)]], address: this.fb.group({ // nested group street: [''], city: ['', Validators.required] }), skills: this.fb.array([]) // dynamic array }); // Access controls get nameCtrl() { return this.form.get('name')!; } get skills() { return this.form.get('skills') as FormArray; } // Add to FormArray dynamically addSkill() { this.skills.push(this.fb.control('', Validators.required)); } // Patch vs setValue loadUser(user: User) { this.form.patchValue(user); // partial update OK this.form.setValue({ name: user.name, email: user.email, ... }); // ALL fields } // Listen to changes constructor() { this.form.get('name')!.valueChanges.pipe( debounceTime(300), distinctUntilChanged() ).subscribe(v => console.log(v)); } onSubmit() { if (this.form.invalid) { this.form.markAllAsTouched(); return; } console.log(this.form.getRawValue()); } }
Custom Validators & Async Validators
VALIDATORS
Custom validatorsTypeScript
// Sync validator function
function noSpacesValidator(control: AbstractControl): ValidationErrors | null {
  if (control.value?.includes(' ')) return { noSpaces: true };
  return null;
}

// Cross-field validator (on FormGroup)
function passwordMatchValidator(group: AbstractControl) {
  const pass = group.get('password')?.value;
  const confirm = group.get('confirmPassword')?.value;
  return pass === confirm ? null : { passwordMismatch: true };
}

// Async validator (e.g. check if username taken)
function usernameAvailable(userService: UserService): AsyncValidatorFn {
  return (control) => timer(300).pipe(
    switchMap(() => userService.checkUsername(control.value)),
    map(taken => taken ? { usernameTaken: true } : null)
  );
}

// Use them
name: ['', [Validators.required, noSpacesValidator],
            [usernameAvailable(this.userService)]]  // 3rd arg = async
Built-in Pipes
PIPES
All built-in pipesHTML
{{ title | uppercase }}                        
{{ name | lowercase }}                         
{{ title | titlecase }}                        

{{ price | currency:'INR':'symbol':'1.2-2' }} 
{{ num | number:'1.2-3' }}                     
{{ percent | percent:'1.0-2' }}               

{{ date | date:'dd/MM/yyyy' }}                
{{ date | date:'short' }}                     
{{ date | date:'fullDate' }}                  

{{ obj | json }}                              
{{ array | slice:0:5 }}                       
{{ observable$ | async }}                     
{{ 'hello' | i18nSelect: {m:'Mr',f:'Ms'} }}   
{{ items | keyvalue }}                        
Custom Pipe โ€” Pure vs Impure
CUSTOM PIPE
Custom pipeTypeScript
@Pipe({
  name: 'truncate',
  standalone: true,
  pure: true  // default: only recalculates when input REFERENCE changes
  // pure: false โ†’ recalculates on EVERY change detection (expensive!)
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, maxLength = 100, suffix = '...'): string {
    if (!value) return '';
    return value.length > maxLength
      ? value.substring(0, maxLength) + suffix
      : value;
  }
}

<!-- Usage -->
{{ description | truncate:50 }}
{{ description | truncate:100:' [more]' }}
Impure pipes run on EVERY change detection cycle. Only use when you MUST react to mutable data changes (like a filter on an array that mutates). Pure pipes are always preferred.
All Lifecycle Hooks โ€” Order & Purpose
LIFECYCLE
Lifecycle hooks in orderTypeScript
export class MyComponent implements
  OnChanges, OnInit, DoCheck, AfterContentInit,
  AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {

  // 1. Called when @Input() values change (even before OnInit)
  ngOnChanges(changes: SimpleChanges) {
    if (changes['userId']?.currentValue) {
      this.loadUser(changes['userId'].currentValue);
    }
  }

  // 2. Called ONCE after first ngOnChanges โ€” your main init hook
  ngOnInit() { this.loadData(); }

  // 3. Every CD cycle (expensive โ€” use sparingly)
  ngDoCheck() { }

  // 4. After projected content (ng-content) init
  ngAfterContentInit() { }

  // 5. After every CD check of projected content
  ngAfterContentChecked() { }

  // 6. After component's VIEW and child views are initialized
  // โœ… @ViewChild properties available HERE
  ngAfterViewInit() { this.chart.render(); }

  // 7. After every check of view and child views
  ngAfterViewChecked() { }

  // 8. Cleanup โ€” unsubscribe, clear timers
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
    clearInterval(this.timer);
  }
}
OnPush Change Detection
PERFORMANCE

Default strategy: Angular checks every component on EVERY event (click, timer, HTTP). With a large app, this is thousands of checks.

OnPush strategy: Component only re-renders when: an @Input() reference changes, an event from inside the component fires, an async pipe emits, or you call markForCheck() manually.

Default = re-checking all 1000 employees when one person sneezes. OnPush = only check an employee when their workload explicitly changes.
OnPush + immutable patternsTypeScript
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
  // โœ… GOOD: new object reference triggers re-render
  @Input() user!: User;

  // In parent: always create NEW objects/arrays
  updateUser() {
    this.user = { ...this.user, name: 'New Name' };  // โœ… new ref
    // this.user.name = 'New Name'  โ† โŒ same ref, won't trigger!
  }
}
Lazy Loading & Preloading Strategies
PERFORMANCE
Preloading strategiesTypeScript
import { PreloadAllModules, NoPreloading } from '@angular/router'; providers: [ provideRouter(routes, withPreloading(PreloadAllModules), // lazy load all in background // withPreloading(NoPreloading) // never preload // custom: only preload flagged routes ) ] // Custom preloading strategy const routes = [ { path: 'admin', loadChildren: () => import('./admin/admin.module'), data: { preload: true } // custom flag } ]; // deferrable views (Angular 17+) @defer (on viewport; prefetch on idle) { <heavy-component /> } @loading { <spinner /> } @placeholder { <div>Coming soon...</div> }
HttpClient โ€” All Methods & Options
HTTP
HttpClient complete referenceTypeScript
export class ApiService {
  private http = inject(HttpClient);

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users', {
      params: { page: '1', limit: '20' },
      headers: { 'X-Custom': 'value' }
    });
  }

  // Observe full response (headers, status code, etc)
  getWithHeaders() {
    return this.http.get<User>('/api/me', { observe: 'response' }).pipe(
      tap(resp => {
        console.log(resp.status);         // 200
        console.log(resp.headers.get('X-Token'));
        console.log(resp.body);           // User object
      })
    );
  }

  // Upload with progress
  upload(file: File) {
    const fd = new FormData();
    fd.append('file', file);
    return this.http.post('/api/upload', fd, {
      observe: 'events',
      reportProgress: true
    }).pipe(
      filter(e => e.type === HttpEventType.UploadProgress),
      map(e => Math.round(100 * e.loaded / (e.total ?? 1)))
    );
  }
}
HTTP Interceptors (Functional)
INTERCEPTORS
Functional interceptor (Angular 15+)TypeScript
// Auth interceptor โ€” add Bearer token to every request export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = inject(AuthService).getToken(); if (!token) return next(req); return next(req.clone({ setHeaders: { Authorization: `Bearer ${token}` } })); }; // Error interceptor โ€” global error handling export const errorInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( catchError((err: HttpErrorResponse) => { if (err.status === 401) inject(Router).navigate(['/login']); if (err.status === 0) inject(NotifyService).error('No connection'); return throwError(() => err); }) ); }; // Register in app.config.ts provideHttpClient(withInterceptors([authInterceptor, errorInterceptor]))
signal, computed, effect
SIGNALS
Signals complete exampleTypeScript
export class CounterComponent { // signal() โ€” writable reactive state count = signal(0); items = signal<string[]>([]); // computed() โ€” derived, auto-updates when deps change doubled = computed(() => this.count() * 2); isEmpty = computed(() => this.items().length === 0); // effect() โ€” side effects when signals change (like useEffect) constructor() { effect(() => { console.log(`Count changed to: ${this.count()}`); // Auto-tracks which signals are read inside }); } increment() { this.count.update(c => c + 1); // based on current this.count.set(10); // set directly } addItem(item: string) { this.items.update(list => [...list, item]); // immutable! } // Convert Observable โ†’ Signal userSignal = toSignal(this.userService.user$, { initialValue: null }); // Convert Signal โ†’ Observable count$ = toObservable(this.count); }
Signal Inputs & Outputs (Angular 17+)
SIGNALS
New signal-based component APITypeScript
export class UserCardComponent { // Signal inputs โ€” auto reactive, no @Input() decorator userId = input.required<string>(); // required theme = input('dark'); // optional with default // Model signal โ€” two-way binding (like ngModel) value = model(''); // two-way [(value)] // Signal output selected = output<string>(); // Computed from signal input displayName = computed(() => `User: ${this.userId()}` // call input like function ); onSelect() { this.selected.emit(this.userId()); } } <!-- Template usage --> <app-user-card [userId]="'123'" [theme]="'light'" [(value)]="myValue" (selected)="onSelected($event)" />